﻿/*                              
   Copyright 2011 Nils Kopal, Christopher Krüger, Simone Sauer, Dennis Nolte, Uni Duisburg-Essen

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Sharebert.OverlayLibrary.API;
using Sharebert.LinkLayerLibrary;
using Sharebert.LinkLayerLibrary.API;
using Sharebert.OverlayLibrary.Messages;
using System.Security.Cryptography;
using System.Numerics;
using System.Net.Sockets;
using System.Net;
using log4net;

namespace Sharebert.OverlayLibrary
{
    public class Overlay : IOverlay
    {
        enum State
        {
            /// <summary>
            /// Overlay is inactive (waiting to be used)
            /// </summary>
            Inactive,

            /// <summary>
            /// Connecting to the overlay
            /// </summary>
            Connecting,

            /// <summary>
            /// Successfully connected
            /// </summary>
            Connected,
            
            /// <summary>
            /// Shuting down the overlay
            /// </summary>
            Shutdown
        }


	private static readonly ILog logger = LogManager.GetLogger(typeof(Overlay));
		
        private const int DEFAULT_PEER_PING_INTERVAL = 15;

        private const int DEFAULT_READ_TIMEOUT = 30;

        private const int DEFAULT_WRITE_TIMEOUT = 5;

        private const int FINGERTABLE_UPDATE_INTERVAL = 25;

        private const int BACKLOG = 30;

        private const Boolean ENABLE_FINGERTABLE = true;

        #region Initialization

        private IPEndPoint graphEndpoint = new IPEndPoint(IPAddress.Parse("255.255.255.255"), 1984);
        private UdpClient graphUDPClient;

        /// <summary>
        /// Initializes a new instance of the overlay. Selects a random port greater than 1023 to listen for new peer connections.
        /// </summary>
        public Overlay(IBootstrapClient bsClient, string listenAddress)
        {
            Initialize(bsClient, listenAddress, 0);
        }

        /// <summary>
        /// Initializes a new instance of the overlay. Uses the specified port to listen for new peer connections.
        /// </summary>
        public Overlay(IBootstrapClient bsClient, string listenAddress, int listenPort)
        {
            Initialize(bsClient, listenAddress, listenPort);
        }

        private void Initialize(IBootstrapClient bsClient, string listenAddress, int listenPort)
        {
            this.ListenAddress = listenAddress;
            this.ListenPort = listenPort;
            this.LocalPeer = new Peer(null, null, this.ListenPort);

            this.Successors = new ThreadSafeList<PeerConnection>();
            this.Predecessors = new ThreadSafeList<PeerConnection>();
            this.linkLayerConnections = new ThreadSafeList<IConnection>();

            this.BootstrapClient = bsClient;
            this.BootstrapClient.ConnectionLost += new EventHandler<EventArgs>(HandleBootstrapTimedOut);
            this.BootstrapClient.NeighboursReceived += new EventHandler<NeighboursReceivedEventArgs>(HandleNeighboursReceived);
            this.BootstrapClient.PeerIdInUseReceived += new EventHandler<EventArgs>(HandlePeerIdInUseReceived);

            this.PeerPingInverval = DEFAULT_PEER_PING_INTERVAL;
            this.ReadTimeout = DEFAULT_READ_TIMEOUT;
            this.WriteTimeout = DEFAULT_WRITE_TIMEOUT;
            this.FingerTableUpdateInterval = FINGERTABLE_UPDATE_INTERVAL * 1000;

            this.listenConnection = new Connection();
            this.listenConnection.NewConnectionEstablished += new EventHandler<ConnectionEventArgs>(HandleNewLinkLayerConnection);

            this.peerPingTimer = new System.Timers.Timer();
            this.peerPingTimer.Elapsed += new System.Timers.ElapsedEventHandler(HandlePeerPingTimerElapsed);
            this.overlayState = State.Inactive;

            this.fingerTableUpdateTimer = new System.Timers.Timer();
            this.fingerTableUpdateTimer.Elapsed += new System.Timers.ElapsedEventHandler(HandleFingerTableUpdateTimerElapsed);
            this.fingerTableUpdateTimer.Interval = this.FingerTableUpdateInterval;
            if (ENABLE_FINGERTABLE)
            {
                this.fingerTableUpdateTimer.Start();
            }
            graphUDPClient = new UdpClient();
        }

        #endregion


        #region Public Interface (Connect, Offline, Send)

        /// <summary>
        /// Connects to the p2p network.
        /// Starts bootstrapping to get some peers.
        /// </summary>
        public void Connect()
        {
            switch (this.overlayState)
            {
                case State.Inactive:
                case State.Shutdown:
                    this.overlayState = State.Connecting;
                    this.ListenPort = this.listenConnection.ListenAsync(System.Net.IPAddress.Parse(this.ListenAddress), BACKLOG);
                    this.LocalPeer = new Peer(PeerID.GetRandomPeerID(), null, this.ListenPort);
                    RequestNeighbours();
                    break;
                default:
                    logger.Debug(String.Format("Already connected | Successors: {0} | Predecessors: {1}", this.Successors.Count, this.Predecessors.Count));
                    break;
            }
        }

        /// <summary>
        /// Perform a clean shutdown, which informs all connected peers.
        /// </summary>
        public void Offline()
        {
            if (this.overlayState == State.Inactive || this.overlayState == State.Shutdown)
            {
                return;
            }
            lock (this.bsLock)
            {
                lock (this.neighbourLock)
                {
                    this.overlayState = State.Shutdown;
                    StopPeerPingTimer();

                    // Shutdown listen connection
                    this.listenConnection.Close();

                    // Shutdown bootstrap client
                    try
                    {
                        this.BootstrapClient.Offline();
                    }
                    catch (NetworkException ex)
                    {
                        logger.Warn("Could not inform bootstrap server that we are are going offline.", ex);
                    }

                    try
                    {
                        if (this.Successors.Count > 0)
                        {
                            // inform first successor that we are going offline
                            var payload = Serialisation.Serialize(this.LocalPeer);
                            var header = new Header(MessageTypes.OFFLINE, payload.Length);
                            var message = new Message(header, payload);
                            this.Successors[0].Connection.SendMessage(message);
                            this.Successors[0].Connection.Close();
                        }
                    }
                    catch (NetworkException ex)
                    {
                        logger.Warn("Could not send offline message to successor!", ex);
                    }

                    // Just close all other connections
                    foreach (var conn in this.Successors)
                    {
                        if (conn.Connection.Connected)
                        {
                            CloseConnection(conn.Connection);
                        }
                        else
                        {
                            logger.Warn("Invalid entry in successor list: " + conn.Connection.ToString());
                        }
                    }
                    this.Successors.Clear();

                    // Just close all other connections
                    foreach (var conn in this.Predecessors)
                    {
                        if (conn.Connection.Connected)
                        {
                            CloseConnection(conn.Connection);
                        }
                        else
                        {
                            logger.Warn("Invalid entry in predecessor list: " + conn.Connection.ToString());
                        }
                    }
                    this.Predecessors.Clear();
                }
            }
        }

        /// <summary>
        /// Sends a message through the overlay.
        /// </summary>
        /// <param name="message">The message to send</param>
        public void Send(IOverlayMessage message)
        {
            PeerConnection conn = DetermineNextHop(message.RecipientID);
            if (conn == null)
            {
                logger.Error("Message sending aborted, because we are the recipient");
                return;
            }
            else
            {
                try
                {
                    var payload = Serialisation.Serialize(message);
                    var header = new Header(MessageTypes.OVERLAY_MESSAGE, payload.Length);
                    var linkLayerMessage = new Message(header, payload);
                    conn.Connection.SendMessage(linkLayerMessage);
                    logger.Debug("Send overlay message to " + conn.RemotePeer.ID);
                }
                catch (NetworkException ex)
                {
                    logger.Error("Could not send message to the overlay!", ex);
                }
                // Here should be some NetworkException handling... if we can not send the message to one peer, we might send it to another.
            }
        }

        #endregion


        #region Bootstrapping

        private void RequestNeighbours()
        {
            if (this.overlayState == State.Shutdown)
            {
                return;
            }

            StopPeerPingTimer();
            PeerConnection.LocalPeer = this.LocalPeer;
            try
            {
                if (this.Predecessors.Count == 0 || this.Successors.Count == 0)
                {
                    logger.Debug(String.Format("Start bootstrapping with PeerID: {0}", this.LocalPeer.ID.ToString()));
                    this.BootstrapClient.RequestNeighbours(this.LocalPeer);
                }
            }
            catch (NetworkException ex)
            {
                logger.Error("Could not request neighbours!", ex);
                if (this.BootstrapServerIsDown != null)
                {
                    this.BootstrapServerIsDown.Invoke(this, new EventArgs());
                }
            }
        }

        private void HandleNeighboursReceived(object sender, NeighboursReceivedEventArgs e)
        {
            // Important note:
            // When the second peer joins the network and would send two join messages to the first peer (one as successor and the other as predecessor), 
            // this peer might become confused which request is the successing connection. So on this host, predecessor and successor could be inversed.
            // Because of this, the join process will be performed asymmetric: The second peer sends a join (on one connection), then the first peer 
            // will send a join on the corresponding connection (to complete the ring).

            this.LocalPeer.IP = e.MyIP;
            if (e.Neighbours.Predecessor != null && e.Neighbours.Successor != null)
            {
                // Connect to predecessor and successor

                if (e.Neighbours.Predecessor.ID.Equals(e.Neighbours.Successor.ID))
                {
                    // Second peer joins the network, only send join to one

                    if (this.LocalPeer.ID > e.Neighbours.Predecessor.ID)
                    {
                        try
                        {
                            var peerConnection = ConnectToPeer(e.Neighbours.Predecessor);
                            RegisterOverlayConnectionEvents(peerConnection);
                            AddPredecessor(peerConnection);
                            logger.Debug(String.Format("Successfully connected to predecessor {0}", peerConnection.RemotePeer.ID));
                        }
                        catch (NetworkException ex)
                        {
                            logger.Error(String.Format("Could not open connection to {0}:{1}", e.Neighbours.Predecessor.IP, e.Neighbours.Predecessor.ListenPort), ex);
                        }
                    }
                    else if (this.LocalPeer.ID < e.Neighbours.Successor.ID)
                    {
                        try
                        {
                            var peerConnection = ConnectToPeer(e.Neighbours.Successor);
                            RegisterOverlayConnectionEvents(peerConnection);
                            AddSuccessor(peerConnection);
                            logger.Debug(String.Format("Successfully connected to successor {0}", peerConnection.RemotePeer.ID));
                        }
                        catch (NetworkException ex)
                        {
                            logger.Error(String.Format("Could not open connection to {0}:{1}", e.Neighbours.Successor.IP, e.Neighbours.Successor.ListenPort), ex);
                        }
                    }
                }
                else
                {
                    // Two or more peers are in the network, so we get two different IDs.

                    // Connect to predecessor
                    try
                    {
                        var peerConnection = ConnectToPeer(e.Neighbours.Predecessor);
                        RegisterOverlayConnectionEvents(peerConnection);
                        AddPredecessor(peerConnection);
                        logger.Debug(String.Format("Successfully connected to predecessor {0}", peerConnection.RemotePeer.ID));
                    }
                    catch (NetworkException ex)
                    {
                        logger.Error(String.Format("Could not open connection to {0}:{1}", e.Neighbours.Predecessor.IP, e.Neighbours.Predecessor.ListenPort), ex);
                    }

                    // Connect to successor
                    try
                    {
                        var peerConnection = ConnectToPeer(e.Neighbours.Successor);
                        RegisterOverlayConnectionEvents(peerConnection);
                        AddSuccessor(peerConnection);
                        logger.Debug(String.Format("Successfully connected to successor {0}", peerConnection.RemotePeer.ID));
                    }
                    catch (NetworkException ex)
                    {
                        logger.Error(String.Format("Could not open connection to {0}:{1}", e.Neighbours.Successor.IP, e.Neighbours.Successor.ListenPort), ex);
                    }
                }
            }
            else
            {
                logger.Debug("Successfully started peer to peer network as first peer.");
            }

            if (this.overlayState == State.Connecting)
            {
                this.overlayState = State.Connected;
            }
            StartPeerPingTimer();
        }

        private void HandlePeerIdInUseReceived(object sender, EventArgs e)
        {
            this.LocalPeer.ID = PeerID.GetRandomPeerID();
            logger.Info("Local PeerID was already in use. Trying to bootstrap with another ID.");
            RequestNeighbours();
        }

        private void HandleBootstrapTimedOut(object sender, EventArgs e)
        {
            if (this.BootstrapServerIsDown != null)
            {
                this.BootstrapServerIsDown.Invoke(this, e);
            }
        }

        #endregion


        #region LinkLayer connection management
        // Necessary when a Link Layer connection has been established, but no overlay message has been received

        private void HandleNewLinkLayerConnection(object sender, ConnectionEventArgs e)
        {
            e.Connection.ConnectionClosed += new EventHandler<EventArgs>(HandleLinkLayerConnectionClosed);
            e.Connection.ConnectionTimedOut += new EventHandler<EventArgs>(HandleLinkLayerConnectionTimedOut);
            e.Connection.MessageReceived += new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);
            e.Connection.ReadTimeout = this.ReadTimeout;
            e.Connection.WriteTimeout = this.WriteTimeout;
            this.linkLayerConnections.Add(e.Connection);
            logger.Debug(String.Format("Link Layer connection to {0}:{1} has been established!", e.Connection.RemoteAddress, e.Connection.RemotePort));
        }

        private void HandleLinkLayerConnectionClosed(object sender, EventArgs e)
        {
            var conn = (IConnection)sender;
            CloseConnection(conn);
            this.linkLayerConnections.Remove(conn);
            logger.Debug(String.Format("Link Layer connection to {0}:{1} has been closed!", conn.RemoteAddress, conn.RemotePort));
        }

        private void HandleLinkLayerConnectionTimedOut(object sender, EventArgs e)
        {
            var conn = (IConnection)sender;
            CloseConnection(conn);
            this.linkLayerConnections.Remove(conn);
            logger.Debug(String.Format("Link Layer connection to {0}:{1} has been timed out!", conn.RemoteAddress, conn.RemotePort));
        }

        private void CloseConnection(IConnection conn)
        {
            conn.ConnectionClosed -= new EventHandler<EventArgs>(HandleLinkLayerConnectionClosed);
            conn.ConnectionTimedOut -= new EventHandler<EventArgs>(HandleLinkLayerConnectionTimedOut);
            conn.MessageReceived -= new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);
            conn.Close();
        }

        #endregion


        #region Overlay: Identify and deserialize messages

        private void HandleMessageReceived(object sender, MessageReceivedEventArgs e)
        {

            var conn = (IConnection)sender;
            if (e.Message.Header.Command.Equals(MessageTypes.JOIN))
            {
                var peer = (Peer)Serialisation.Deserialize(e.Message.Payload, typeof(Peer));
                logger.Debug(String.Format("Received Join from peer {0}:{1} [{2}]", conn.RemoteAddress, conn.RemotePort, peer.ID));

                var peerConn = new PeerConnection(peer, conn);
                RegisterOverlayConnectionEvents(peerConn);
                ProcessJoinReceived(peerConn);
            }
            else if (e.Message.Header.Command.Equals(MessageTypes.FINGER_JOIN))
            {
                var peer = (Peer)Serialisation.Deserialize(e.Message.Payload, typeof(Peer));
                logger.Debug(String.Format("Received Finger-Join from peer {0}:{1} [{2}]", conn.RemoteAddress, conn.RemotePort, peer.ID));

                var peerConn = new PeerConnection(peer, conn);
                RegisterOverlayConnectionEvents(peerConn);
                ProcessFingerJoinReceived(peerConn);
            }
            else if (e.Message.Header.Command.Equals(MessageTypes.OVERLAY_MESSAGE))
            {
                ProcessOverlayMessageReceived(e.Message);
            }
            else if (e.Message.Header.Command.Equals(MessageTypes.FINGER_SEARCH))
            {
                ProcessFingerSearchMessageReveiced(e.Message);
            }
        }

        #endregion
        
        #region Overlay: Join

        private void ProcessJoinReceived(PeerConnection newPeerConnection)
        {
            // Important notes: 
            // A: With 'weldseam' I will mean the connection between smallest id and the highest id (that closes the ring)
            // B: CloseOnJoin messages will only be send to successors

            lock (this.neighbourLock)
            {
                bool succeeded = true;
                if (newPeerConnection.RemotePeer.ID.Equals(this.LocalPeer.ID))
                {
                    // new peer's id is the same as our id... wtf?

                    logger.Warn(String.Format("Peer {0}:{1} ({2}) attempted to become our nighbour, but is using our ID!", newPeerConnection.Connection.RemoteAddress, newPeerConnection.Connection.RemotePort, newPeerConnection.RemotePeer.ID));

                    // Maybe we should send him an error message
                    DeregisterOverlayConnectionEvents(newPeerConnection);
                    newPeerConnection.Connection.Close();
                    return;
                }
                if (this.Predecessors.Count == 0 && this.Successors.Count == 0)
                {
                    // Second peer joins: We are the first peer and just received a join from the second (new) peer
                    PerformAsymmetricJoin(newPeerConnection);
                }
                else if (this.Predecessors.Count == 0)
                {
                    // Second peer joins: We are the second peer and received the asymmetric join from the first peer
                    AddPredecessor(newPeerConnection);
                    logger.Debug(String.Format("Asymmetric join performed! Added peer {0} as predecessor.", newPeerConnection.RemotePeer.ID));
                }
                else if (this.Successors.Count == 0)
                {
                    // Second peer joins: We are the second peer and received the asymmetric join from the first peer
                    AddSuccessor(newPeerConnection);
                    logger.Debug(String.Format("Asymmetric join performed! Added peer {0} as successor.", newPeerConnection.RemotePeer.ID));
                }
                else
                {
                    // Two or more peers are in the ring: Now we need to perform a CloseOnJoin

                    if (this.Predecessors[0].RemotePeer.ID.Equals(this.Successors[0].RemotePeer.ID))
                    {
                        // Third peer joins: Both peers are successors of each other, so only that with the lower ID will request a CloseOnJoin

                        if (newPeerConnection.RemotePeer.ID > this.LocalPeer.ID && newPeerConnection.RemotePeer.ID < this.Successors[0].RemotePeer.ID ||
                            newPeerConnection.RemotePeer.ID < this.LocalPeer.ID && newPeerConnection.RemotePeer.ID > this.Successors[0].RemotePeer.ID)
                        {
                            // new peer's ID is lying between this peer's ID and that of the successor

                            if (this.LocalPeer.ID < this.Successors[0].RemotePeer.ID)
                            {
                                succeeded = this.Successors[0].RequestCloseOnJoin(newPeerConnection);
                            }
                            else
                            {
                                succeeded = this.Predecessors[0].NewConnectionEstablished(newPeerConnection);
                            }
                        }
                        else
                        {
                            // new peer is joining on the weldseam
                            if (this.LocalPeer.ID > this.Successors[0].RemotePeer.ID)
                            {
                                succeeded = this.Successors[0].RequestCloseOnJoin(newPeerConnection);
                            }
                            else
                            {
                                succeeded = this.Predecessors[0].NewConnectionEstablished(newPeerConnection);
                            }
                        }
                        return;
                    }

                    // More than two peers are in the ring

                    if (newPeerConnection.RemotePeer.ID > this.LocalPeer.ID && newPeerConnection.RemotePeer.ID < this.Successors[0].RemotePeer.ID)
                    {
                        // New peer is our regular new successor
                        // Current structure: +- 100 --- ME --- 200 -+
                        // New peer:                         ^
                        succeeded = this.Successors[0].RequestCloseOnJoin(newPeerConnection);
                    }
                    else if(newPeerConnection.RemotePeer.ID < this.LocalPeer.ID && newPeerConnection.RemotePeer.ID > this.Predecessors[0].RemotePeer.ID)
                    {
                        // New peer is our regular new predecessor
                        // Current structure: +- 100 --- ME --- 200 -+
                        // New peer:                  ^
                        succeeded = this.Predecessors[0].NewConnectionEstablished(newPeerConnection);
                    }
                    else
                    {
                        // We are at the weldseam

                        if (this.Predecessors[0].RemotePeer.ID > this.LocalPeer.ID)
                        {
                            // If our predecessor has a higher id than we have, we must lie on the 'left side'

                            if (newPeerConnection.RemotePeer.ID < this.LocalPeer.ID && newPeerConnection.RemotePeer.ID < this.Predecessors[0].RemotePeer.ID)
                            {
                                // Current structure: +- ME --- 150 --- 200 -+
                                // New peer:          ^
                                succeeded = this.Predecessors[0].NewConnectionEstablished(newPeerConnection);
                            }
                            else if (newPeerConnection.RemotePeer.ID > this.LocalPeer.ID && newPeerConnection.RemotePeer.ID > this.Predecessors[0].RemotePeer.ID)
                            {
                                // Current structure: +- ME --- 150 --- 200 -+
                                // New peer:                                 ^
                                succeeded = this.Predecessors[0].NewConnectionEstablished(newPeerConnection);
                            }
                            else
                            {
                                logger.Warn(String.Format("Error #1 while peer {0}:{1} ({2}) attempted to join the ring at the wrong place! (Predecessor: {3} | This: {4} | Successor: {5})",
                                    newPeerConnection.Connection.RemoteAddress, newPeerConnection.Connection.RemotePort, newPeerConnection.RemotePeer.ID,
                                    this.Predecessors[0].RemotePeer.ID, this.LocalPeer.ID, this.Successors[0].RemotePeer.ID));
                            }
                        }
                        else if (this.Successors[0].RemotePeer.ID < this.LocalPeer.ID)
                        {
                            // If our successor has a lower id than we have, we must lie on the 'right side'

                            if (newPeerConnection.RemotePeer.ID < this.LocalPeer.ID && newPeerConnection.RemotePeer.ID < this.Successors[0].RemotePeer.ID)
                            {
                                // Current structure: +- 100 --- 150 --- ME -+
                                // New peer:          ^
                                succeeded = this.Successors[0].RequestCloseOnJoin(newPeerConnection);
                            }
                            else if (newPeerConnection.RemotePeer.ID > this.LocalPeer.ID && newPeerConnection.RemotePeer.ID > this.Successors[0].RemotePeer.ID)
                            {
                                // Current structure: +- 100 --- 150 --- ME -+
                                // New peer:                                 ^
                                succeeded = this.Successors[0].RequestCloseOnJoin(newPeerConnection);
                            }
                            else
                            {
                                logger.Warn(String.Format("Error #2 while peer {0}:{1} ({2}) attempted to join the ring at the wrong place! (Predecessor: {3} | This: {4} | Successor: {5})",
                                    newPeerConnection.Connection.RemoteAddress, newPeerConnection.Connection.RemotePort, newPeerConnection.RemotePeer.ID,
                                    this.Predecessors[0].RemotePeer.ID, this.LocalPeer.ID, this.Successors[0].RemotePeer.ID));
                            }
                        }
                        else
                        {
                            logger.Warn(String.Format("Error #3 while peer {0}:{1} ({2}) attempted to join the ring at the wrong place! (Predecessor: {3} | This: {4} | Successor: {5})",
                                newPeerConnection.Connection.RemoteAddress, newPeerConnection.Connection.RemotePort, newPeerConnection.RemotePeer.ID,
                                this.Predecessors[0].RemotePeer.ID, this.LocalPeer.ID, this.Successors[0].RemotePeer.ID));

                            //logger.Warn(String.Format("Peer {0}:{1} ({2}) attempted to join the ring at the wrong place! (Predecessor: {3} | Successor: {4})",
                            //    newPeerConnection.Connection.RemoteAddress, newPeerConnection.Connection.RemotePort, newPeerConnection.RemotePeer.ID,
                            //    this.Predecessors[0].RemotePeer.ID, this.Successors[0].RemotePeer.ID));

                            // Maybe we should send him an error message
                            //DeregisterOverlayConnectionEvents(newPeerConnection);
                            //newPeerConnection.Connection.Close();
                        }
                        
                    }

                }
                if (!succeeded)
                {
                    logger.Info("Could not perform close on join, because a close on join operation is in progress!");
                }

            }
        }

        private void ProcessFingerJoinReceived(PeerConnection newPeerConnection)
        {
            AddSuccessor(newPeerConnection);
            logger.Debug(String.Format("Peer {0}:{1} ({2}) added to our successors", newPeerConnection.Connection.RemoteAddress, newPeerConnection.Connection.RemotePort, newPeerConnection.RemotePeer.ID));
        }

        private void HandleCloseOnJoinFinished(object sender, PeerConnectionEventArgs e)
        {
            PeerConnection oldPeerConnection = (PeerConnection)sender;
            bool replaced = ReplacePeerConnection(oldPeerConnection, e.Connection);
            if (replaced)
            {
                logger.Debug(String.Format("Replaced peer {0} by {1} | New structure: Predecessor {2} <-> This {3} <-> Successor {4}",
                    oldPeerConnection.RemotePeer.ID, e.Connection.RemotePeer.ID, this.Predecessors[0].RemotePeer.ID, this.LocalPeer.ID, this.Successors[0].RemotePeer.ID));
            }
            else
            {
                logger.Warn(String.Format("Could not replace old overlay connection to peer {0}", oldPeerConnection.RemotePeer.ID));
            }
        }

        private void PerformAsymmetricJoin(PeerConnection newPeerConnection)
        {
            if (newPeerConnection.RemotePeer.ID < this.LocalPeer.ID)
            {
                AddPredecessor(newPeerConnection);
                PeerConnection successorConn = ConnectToPeer(newPeerConnection.RemotePeer);
                RegisterOverlayConnectionEvents(successorConn);
                AddSuccessor(successorConn);
                logger.Debug(String.Format("Added peer {0} as predescessor. Now performing asymmetric join!", newPeerConnection.RemotePeer.ID));
            }
            else if (newPeerConnection.RemotePeer.ID > this.LocalPeer.ID)
            {
                AddSuccessor(newPeerConnection);
                PeerConnection predecessorConn = ConnectToPeer(newPeerConnection.RemotePeer);
                RegisterOverlayConnectionEvents(predecessorConn);
                AddPredecessor(predecessorConn);
                logger.Debug(String.Format("Added peer {0} as successor. Now performing asymmetric join!", newPeerConnection.RemotePeer.ID));
            }
            else
            {
                logger.Warn(String.Format("Peer {0}:{1} ({2}) attempted to become our neighbour, but has our ID!", newPeerConnection.Connection.RemoteAddress, newPeerConnection.Connection.RemotePort, newPeerConnection.RemotePeer.ID));
            }
        }

        private void ProcessFingerSearchMessageReveiced(IMessage iMessage)
        {
            var fingerSearchMessage = (FingerSearchMessage)Serialisation.Deserialize(iMessage.Payload, typeof(FingerSearchMessage));

            var con = DetermineNextHop(fingerSearchMessage.RecipientID);
            if (con != null) //this message is not for us, so we send it to next one
            {
                var payload = Serialisation.Serialize(fingerSearchMessage);
                var header = new Header(MessageTypes.FINGER_SEARCH, payload.Length);
                var message = new Message(header, payload);                
                con.Connection.SendMessage(message);
                return;
            }

            foreach (PeerConnection conn in Predecessors)
            {
                if (conn.RemotePeer.ID.Equals(fingerSearchMessage.SenderID))
                {                    
                    return; // we are already connected to this predecessor so we abort
                }                                
            }

            //we are not connected to this predecessor so we establish a new connection
            var peer = new Peer(fingerSearchMessage.SenderID, fingerSearchMessage.SenderIP, fingerSearchMessage.SenderPort);
            var predecessorConn = ConnectToFingerPeer(peer);
            AddPredecessor(predecessorConn);
            logger.Debug(String.Format("Peer {0}:{1} ({2}) added to our predecessors", predecessorConn.Connection.RemoteAddress, predecessorConn.Connection.RemotePort, predecessorConn.RemotePeer.ID));
        }

        #endregion

        #region Overlay: Ping and Offline

        private void HandleOverlayConnectionClosed(object sender, EventArgs e)
        {
            var conn = (PeerConnection)sender;
            logger.Debug(String.Format("Overlay connection to peer {0} has been closed!", conn.RemotePeer.ID));
            RemoveOverlayConnection(conn);
            DeregisterOverlayConnectionEvents(conn);
        }

        private void HandleOverlayConnectionTimedOut(object sender, EventArgs e)
        {
            var conn = (PeerConnection)sender;
            logger.Debug(String.Format("Overlay connection to peer {0} has timed out!", conn.RemotePeer.ID));
            RemoveOverlayConnection(conn);
            DeregisterOverlayConnectionEvents(conn);
        }

        private void HandlePeerIsOffline(object sender, EventArgs e)
        {
            var conn = (PeerConnection)sender;
            logger.Debug(String.Format("Peer {0} has gone offline!", conn.RemotePeer.ID));
            RemoveOverlayConnection(conn);
            DeregisterOverlayConnectionEvents(conn);
        }

        private void HandlePeerPingTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
        {
            StopPeerPingTimer();
            PingNeighbours();
            StartPeerPingTimer();
        }

        private void HandleFingerTableUpdateTimerElapsed(object sender, System.Timers.ElapsedEventArgs e)
        {           
            logger.Debug("Send out FingerSearchMessages to update finger table");
            try
            {
                FingerSearchMessage fingerSearchMessage = new FingerSearchMessage();
                fingerSearchMessage.SenderID = this.LocalPeer.ID;
                fingerSearchMessage.SenderIP = this.LocalPeer.IP;
                fingerSearchMessage.SenderPort = this.LocalPeer.ListenPort;

                int n = PeerID.PEER_ID_BYTE_SIZE * 8;
                BigInteger mod = BigInteger.Pow(new BigInteger(2), n);

                for (int i = 2; i <= n; i++)
                {
                    var big = LocalPeer.ID.BigIntegerValue + BigInteger.Pow(new BigInteger(2), i);
                    big = BigInteger.Remainder(big, mod);
                    fingerSearchMessage.RecipientID = new PeerID(big);

                    var payload = Serialisation.Serialize(fingerSearchMessage);
                    var header = new Header(MessageTypes.FINGER_SEARCH, payload.Length);
                    var message = new Message(header, payload);
                    var con = DetermineNextHop(fingerSearchMessage.RecipientID);
                    if (con != null)
                    {
                        con.Connection.SendMessage(message);
                    }
                }
            }
            catch (Exception ex)
            {
                logger.Error("Exception during sending out FingerSearchMessages to update finger table", ex);
            }
        }

        private void PingNeighbours()
        {
            var payload = Serialisation.Serialize(this.LocalPeer);
            var header = new Header(MessageTypes.PING, payload.Length);
            var message = new Message(header, payload);
            ASCIIEncoding enc = new ASCIIEncoding();

            lock (this.neighbourLock)
            {
                if (this.Predecessors.Count > 0)
                {
                    StringBuilder sb = new StringBuilder();
                    sb.Append("Send Ping message to predecessors ( ");
                    foreach (var peer in this.Predecessors)
                    {
                        try
                        {
                            peer.Connection.SendMessage(message);                            
                            sb.Append(peer.Connection.RemoteAddress).Append(":").Append(peer.Connection.RemotePort);
                            sb.Append(" [").Append(peer.RemotePeer.ID).Append("] ");

                            StringBuilder s = new StringBuilder();
                            s.Append(peer.RemotePeer.ID);                    
                            s.Append(",");
                            s.Append(LocalPeer.ID.ToString());
                            var bytes = enc.GetBytes(s.ToString());
                            graphUDPClient.Send(bytes, bytes.Length, graphEndpoint);
                        }
                        catch (Exception ex)
                        {
                            logger.Warn(String.Format("Error while pinging peer {0}:{1} [{2}]!", peer.Connection.RemoteAddress, peer.Connection.RemotePort, peer.RemotePeer.ID), ex);
                        }
                    }
                    sb.Append(")");
                    logger.Debug(sb.ToString());
                }

                if (this.Successors.Count > 0)
                {                    
                    StringBuilder sb = new StringBuilder();
                    sb.Append("Send Ping message to successors ( ");
                    foreach (var peer in this.Successors)
                    {
                        try
                        {
                            peer.Connection.SendMessage(message);
                            sb.Append(peer.Connection.RemoteAddress).Append(":").Append(peer.Connection.RemotePort);
                            sb.Append(" [").Append(peer.RemotePeer.ID).Append("] ");

                            StringBuilder s = new StringBuilder();
                            s.Append(LocalPeer.ID.ToString());
                            s.Append(",");
                            s.Append(peer.RemotePeer.ID);
                            var bytes = enc.GetBytes(s.ToString());
                            graphUDPClient.Send(bytes, bytes.Length, graphEndpoint);

                        }
                        catch (Exception ex)
                        {
                            logger.Warn(String.Format("Error while pinging peer {0}:{1} [{2}]!", peer.Connection.RemoteAddress, peer.Connection.RemotePort, peer.RemotePeer.ID), ex);
                        }
                    }
                    sb.Append(")");
                    logger.Debug(sb.ToString());
                }
            }
        }

        private void StartPeerPingTimer()
        {
            if (!this.peerPingTimer.Enabled && (this.overlayState == State.Connecting || this.overlayState == State.Connected))
            {
                this.peerPingTimer.Interval = this.PeerPingInverval * 1000;
                this.peerPingTimer.Start();
            }
        }

        private void StopPeerPingTimer()
        {
            if (this.peerPingTimer.Enabled)
            {
                this.peerPingTimer.Stop();
            }
        }

        private PeerConnection ConnectToPeer(IPeer remotePeer)
        {
            // Connect to the peer
            var connection = new Connection();
            connection.ConnectionClosed += new EventHandler<EventArgs>(HandleLinkLayerConnectionClosed);
            connection.ConnectionTimedOut += new EventHandler<EventArgs>(HandleLinkLayerConnectionTimedOut);
            connection.MessageReceived += new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);
            connection.ReadTimeout = this.ReadTimeout;
            connection.WriteTimeout = this.WriteTimeout;
            connection.Open(remotePeer.IP, remotePeer.ListenPort);

            // Directly send a message to propagate our PeerID
            var peerInfo = Serialisation.Serialize(this.LocalPeer);
            var header = new Header(MessageTypes.JOIN, peerInfo.Length);
            var message = new Message(header, peerInfo);
            connection.SendMessage(message);

            return new PeerConnection(remotePeer, connection);
        }

        private PeerConnection ConnectToFingerPeer(IPeer remotePeer)
        {
            // Connect to the peer
            var connection = new Connection();
            connection.ConnectionClosed += new EventHandler<EventArgs>(HandleLinkLayerConnectionClosed);
            connection.ConnectionTimedOut += new EventHandler<EventArgs>(HandleLinkLayerConnectionTimedOut);
            connection.MessageReceived += new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);
            connection.ReadTimeout = this.ReadTimeout;
            connection.WriteTimeout = this.WriteTimeout;
            connection.Open(remotePeer.IP, remotePeer.ListenPort);

            // Directly send a message to propagate our PeerID
            var peerInfo = Serialisation.Serialize(this.LocalPeer);
            var header = new Header(MessageTypes.FINGER_JOIN, peerInfo.Length);
            var message = new Message(header, peerInfo);
            connection.SendMessage(message);

            return new PeerConnection(remotePeer, connection);
        }

        #endregion 


        #region Overlay: Routing

        /// <summary>
        /// Determines the nearest known peer for a given ID. Returns null if this host is responsible.
        /// </summary>
        /// <param name="recipientID">The ID that we are want to route messages to</param>
        /// <returns>A peer connection to the next nearest neighbour or null if we are responsible</returns>
        /// <exception cref="RingIsBrokenException">Ring is broken</exception>
        private PeerConnection DetermineNextHop(PeerID recipientID)
        {
            lock (this.neighbourLock)
            {
                if (this.Predecessors.Count == 0 && this.Successors.Count == 0)
                {
                    // No other peer, we are responsible
                    return null;
                }
                else if (this.Predecessors.Count == 0)
                {
                    // Ring is broken
                    throw new RingIsBrokenException("No predecessor set! Ring seems to be broken!");
                }
                else if (this.Successors.Count == 0)
                {
                    // Ring is broken
                    throw new RingIsBrokenException("No successors set! Ring seems to be broken!");
                }

                // First we have to check if we lie at the weldseam...

                if (this.Successors[0].RemotePeer.ID < this.LocalPeer.ID)
                {
                    // We are at the weldseam ('right side')

                    if (recipientID > this.LocalPeer.ID || recipientID < this.Successors[0].RemotePeer.ID)
                    {
                        // The ID is higher than our id or smaller than that of our successor... so the successor is responsible
                        // Structure:       - 100 --- 200 --- ME -
                        // Recipient ID:    ^                    ^
                        return this.Successors[0];
                    }
                    else if (recipientID <= this.LocalPeer.ID && recipientID > this.Predecessors[0].RemotePeer.ID)
                    {
                        // We are responsible
                        // Structure:       - 100 --- 200 --- ME -
                        // Recipient ID:                   ^
                        return null;
                    }
                    else
                    {
                        // The ID is 'behind the responsibility of our next successor
                        // Structure:       - 100 --- 200 --- ME -
                        // Recipient ID:           ^

                        PeerConnection nextHop = this.Successors[0];
                        foreach (var peer in this.Successors)
                        {
                            if (recipientID > peer.RemotePeer.ID && peer.RemotePeer.ID > nextHop.RemotePeer.ID)
                            {
                                nextHop = peer;
                            }
                        }
                        return nextHop;
                    }
                }
                else if (this.Predecessors[0].RemotePeer.ID > this.LocalPeer.ID)
                {
                    // We are at the weldseam ('left side')

                    if (recipientID <= this.LocalPeer.ID || recipientID > this.Predecessors[0].RemotePeer.ID)
                    {
                        // The ID is smaller than our id or higher than that of our predecessor... we are responsible
                        // Structure:       - ME --- 100 --- 200 -
                        // Recipient ID:    ^                    ^
                        return null;
                    }
                    else if (recipientID > this.LocalPeer.ID && recipientID <= this.Successors[0].RemotePeer.ID)
                    {
                        // Our direct successor is responsible
                        // Structure:       - ME --- 100 --- 200 -
                        // Recipient ID:          ^
                        return this.Successors[0];
                    }
                    else
                    {
                        // The ID is 'behind the responsibility of our next successor
                        // Structure:       - ME --- 100 --- 200 -
                        // Recipient ID:                  ^

                        PeerConnection nextHop = this.Successors[0];
                        foreach (var peer in this.Successors)
                        {
                            if (recipientID > peer.RemotePeer.ID && peer.RemotePeer.ID > nextHop.RemotePeer.ID)
                            {
                                nextHop = peer;
                            }
                        }
                        return nextHop;
                    }
                }
                else
                {
                    // We have 'regular' neighbours

                    if (recipientID > this.LocalPeer.ID)
                    {
                        // Recipient ID is higher than our ID. Forward to the successing peer with the nearest smaller ID
                        PeerConnection nextHop = this.Successors[0];
                        foreach (var peer in this.Successors)
                        {
                            if (recipientID > peer.RemotePeer.ID && peer.RemotePeer.ID > nextHop.RemotePeer.ID)
                            {
                                nextHop = peer;
                            }
                        }
                        return nextHop;
                    }
                    else
                    {
                        // Recipient ID is smaller than our ID

                        if (recipientID > this.Predecessors[0].RemotePeer.ID)
                        {
                            // We are responsible
                            return null;
                        }

                        PeerConnection nextHop = this.Successors[0];
                        foreach (var peer in this.Successors)
                        {
                            // Search our successor with the nearest smaller ID than the recipient ID
                            if (recipientID < peer.RemotePeer.ID && peer.RemotePeer.ID > nextHop.RemotePeer.ID)
                            {
                                nextHop = peer;
                            }
                        }
                        return nextHop;
                    }
                }
            }
        }

        #endregion


        #region DHT: Forward messages

        private void ProcessOverlayMessageReceived(IMessage message)
        {
            try
            {
                var overlayMessage = (OverlayMessage)Serialisation.Deserialize(message.Payload, typeof(OverlayMessage));
                var nextHop = DetermineNextHop(overlayMessage.RecipientID);
                if (nextHop == null)
                {
                    // We are responsible, so forwad to the above layer
                    if (this.OverlayMessageReceived != null)
                    {
                        this.OverlayMessageReceived.Invoke(this, new OverlayMessageReceivedEventArgs(overlayMessage, message.Payload));
                    }
                }
                else
                {
                    // Forward the message to the next hop
                    nextHop.Connection.SendMessage(message);
                }
            }
            catch (SerializationException ex)
            {
                logger.Error("Deserialization of an overlay message has failed", ex);
            }
            catch (NetworkException ex)
            {
                logger.Error("Could not forward message", ex);
            }
        }

        #endregion


        #region Neighbour list

        private void AddSuccessor(PeerConnection newSuccessor)
        {
            lock (this.neighbourLock)
            {
                this.Successors.Add(newSuccessor);
            }

            ASCIIEncoding enc = new ASCIIEncoding();
            StringBuilder s = new StringBuilder();
            s.Append(LocalPeer.ID.ToString());
            s.Append(",");
            s.Append(newSuccessor.RemotePeer.ID);
            var bytes = enc.GetBytes(s.ToString());
            graphUDPClient.Send(bytes, bytes.Length, graphEndpoint);

            this.linkLayerConnections.Remove(newSuccessor.Connection);
            newSuccessor.Connection.ConnectionClosed -= new EventHandler<EventArgs>(HandleLinkLayerConnectionClosed);
            newSuccessor.Connection.ConnectionTimedOut -= new EventHandler<EventArgs>(HandleLinkLayerConnectionTimedOut);
        }

        private void AddPredecessor(PeerConnection newPredecessor)
        {
            lock (this.neighbourLock)
            {
                this.Predecessors.Add(newPredecessor);
            }

            ASCIIEncoding enc = new ASCIIEncoding();
            StringBuilder s = new StringBuilder();
            s.Append(newPredecessor.RemotePeer.ID);
            s.Append(",");
            s.Append(LocalPeer.ID.ToString());
            var bytes = enc.GetBytes(s.ToString());
            graphUDPClient.Send(bytes, bytes.Length, graphEndpoint);

            this.linkLayerConnections.Remove(newPredecessor.Connection);
            newPredecessor.Connection.ConnectionClosed -= new EventHandler<EventArgs>(HandleLinkLayerConnectionClosed);
            newPredecessor.Connection.ConnectionTimedOut -= new EventHandler<EventArgs>(HandleLinkLayerConnectionTimedOut);
        }

        private void RegisterOverlayConnectionEvents(PeerConnection conn)
        {
            conn.CloseOnJoinFinished += new EventHandler<PeerConnectionEventArgs>(HandleCloseOnJoinFinished);
            conn.ConnectionClosed += new EventHandler<EventArgs>(HandleOverlayConnectionClosed);
            conn.ConnectionTimedOut += new EventHandler<EventArgs>(HandleOverlayConnectionTimedOut);
            conn.PeerIsOffline += new EventHandler<EventArgs>(HandlePeerIsOffline);
        }

        private void DeregisterOverlayConnectionEvents(PeerConnection conn)
        {
            conn.CloseOnJoinFinished -= new EventHandler<PeerConnectionEventArgs>(HandleCloseOnJoinFinished);
            conn.ConnectionClosed -= new EventHandler<EventArgs>(HandleOverlayConnectionClosed);
            conn.ConnectionTimedOut -= new EventHandler<EventArgs>(HandleOverlayConnectionTimedOut);
            conn.PeerIsOffline -= new EventHandler<EventArgs>(HandlePeerIsOffline);
        }

        private void RemoveOverlayConnection(PeerConnection conn)
        {
            lock (this.neighbourLock)
            {
                ASCIIEncoding enc = new ASCIIEncoding();
                PeerConnection removePeer = null;
                foreach (PeerConnection peer in this.Predecessors)
                {
                    if (peer.Equals(conn))
                    {
                        removePeer = peer;
                        break;
                    }
                }
                if (removePeer != null)
                {
                    this.Predecessors.Remove(removePeer);

                    StringBuilder s = new StringBuilder();
                    s.Append(removePeer.RemotePeer.ID.ToString()); 
                    s.Append(",");
                    s.Append(LocalPeer.ID);
                    s.Append(",");
                    s.Append("Remove");
                    var bytes = enc.GetBytes(s.ToString());
                    graphUDPClient.Send(bytes, bytes.Length, graphEndpoint);

                }
                removePeer = null;
                foreach (PeerConnection peer in this.Successors)
                {
                    if (peer.Equals(conn))
                    {
                        removePeer = peer;
                        break;
                    }
                }
                if (removePeer != null)
                {
                    this.Successors.Remove(removePeer);

                    StringBuilder s = new StringBuilder();
                    s.Append(LocalPeer.ID); 
                    s.Append(",");
                    s.Append(removePeer.RemotePeer.ID.ToString());
                    s.Append(",");
                    s.Append("Remove");
                    var bytes = enc.GetBytes(s.ToString());
                    graphUDPClient.Send(bytes, bytes.Length, graphEndpoint);
                }
            }
        }

        private bool ReplacePeerConnection(PeerConnection oldConnection, PeerConnection newConnection)
        {
            lock (this.neighbourLock)
            {
                int successorIndex = -1;
                int predecessorIndex = -1;
                for (int i = 0; i < this.Successors.Count; i++)
                {
                    if (this.Successors[i] == oldConnection)
                    {
                        successorIndex = i;
                        break;
                    }
                }
                for (int i = 0; i < this.Predecessors.Count; i++)
                {
                    if (this.Predecessors[i] == oldConnection)
                    {
                        predecessorIndex = i;
                        break;
                    }
                }
                if (predecessorIndex > -1 && successorIndex > -1 && this.Predecessors[predecessorIndex].RemotePeer.ID.Equals(this.Successors[successorIndex].RemotePeer.ID))
                {
                    // Only two peers in the ring, so we need to replace the 'wrong'

                    if (this.LocalPeer.ID < this.Predecessors[predecessorIndex].RemotePeer.ID)
                    {
                        this.Predecessors[predecessorIndex] = newConnection;
                    }
                    else
                    {
                        this.Successors[successorIndex] = newConnection;
                    }
                    return true;
                }
                if (predecessorIndex > -1)
                {
                    // Found a predecessor to replace
                    this.Predecessors[predecessorIndex] = newConnection;
                    return true;
                }
                if (successorIndex > -1)
                {
                    // Found a successor to replace
                    this.Successors[successorIndex] = newConnection;
                    return true;
                }
                return false;
            }
        }

        #endregion


        #region Properties and private member

        /// <summary>
        /// The used bootstrap client.
        /// </summary>
        public IBootstrapClient BootstrapClient { get; private set; }

        /// <summary>
        /// The IP address or hostname of the bootstrap server.
        /// </summary>
        public string BootstrapServerAddress { get; private set; }

        /// <summary>
        /// The port number of the bootstrap server.
        /// </summary>
        public int BootstrapServerPort { get; private set; }

        /// <summary>
        /// The IP address used to listen for new connections.
        /// </summary>
        public string ListenAddress { get; private set; }

        /// <summary>
        /// The port number used to listen for new connections.
        /// </summary>
        public int ListenPort { get; private set; }

        /// <summary>
        /// The Peer object of this host.
        /// </summary>
        public IPeer LocalPeer { get; private set; }

        /// <summary>
        /// Receiving messages will timeout after the specified time in seconds. 
        /// Peers that do not send us a ping message within this time are assumed to be down.
        /// </summary>
        public int ReadTimeout { get; set; }

        /// <summary>
        /// Trying to send messages will timeout after the specified time in seconds.
        /// </summary>
        public int WriteTimeout { get; set; }

        /// <summary>
        /// Time in seconds, the peer will ping other peers.
        /// </summary>
        public int PeerPingInverval { get; set; }

        /// <summary>
        /// Time in seconds, the peer will try to update its finger table
        /// </summary>
        public int FingerTableUpdateInterval { get; set; }

        /// <summary>
        /// A list of successor peers. Those peers represent our finger table.
        /// </summary>
        public ThreadSafeList<PeerConnection> Successors { get; private set; }

        /// <summary>
        /// A list of predecessor peers. Those peers referencing us in their finger table.
        /// </summary>
        public ThreadSafeList<PeerConnection> Predecessors { get; private set; }

        private State overlayState;

        private IConnection listenConnection;

        // Active LinkLayer connections, where no join has been received
        private ThreadSafeList<IConnection> linkLayerConnections;

        private System.Timers.Timer peerPingTimer;

        private System.Timers.Timer fingerTableUpdateTimer;

        private object bsLock = new object();

        private object neighbourLock = new object();

        #endregion


        #region Events

        /// <summary>
        /// The bootstrap server is down.
        /// </summary>
        public event EventHandler<EventArgs> BootstrapServerIsDown;

        /// <summary>
        /// Received a message from the overlay.
        /// </summary>
        public event EventHandler<OverlayMessageReceivedEventArgs> OverlayMessageReceived;

        #endregion
    }
}
